﻿using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Collections.Generic;
using Amercom.Silverlight.Lib;
using Amercom.Silverlight.Helpers.PanoramaCube;

namespace Amercom.Silverlight.Controls
{

    /*
     * ****************************
     * PanoramaCube, Silverlight version
     * By Martijn Croezen and Rob Houweling
     * 
     * martijn@amercom.nl / http://whizzkid74.blogspot.com
     * rob@amercom.nl / http://web-snippets.blogspot.com
     * (c) 2008 Amercom B.V. http://www.amercom.nl
     * **************************
    */

    public class PanoramaCube : Control
    {
        #region Properties
        public bool DebugEnabled { get; set; }
        public List<Uri> ImageList { get; set; }

        private bool _enableRendermodeSwitch = false;
        /// <summary>
        /// enable the user to change rendering method by pressing T
        /// </summary>
        public bool EnableRendermodeSwitch 
        { 
            get
            {
                return _enableRendermodeSwitch;
            }
            set
            {
                _enableRendermodeSwitch = value;
            }
        }

        private int _fps = 30;
        public int FPS
        {
            get
            {
                return _fps;
            }
            internal set 
            {
                _fps = value;
            }
        }

        private int _subdivisions = 6;
        public int Subdivisions 
        {
            get
            {
                return _subdivisions;
            }
            set
            {
                _subdivisions = value;
            }
        }
        #endregion

        #region Fields
        // The panoramacube's size (actually, half its size, the cube coords go from -cubeSize to +cubeSize;
        private static readonly double _cubeXSize = 300;
        private static readonly double _cubeYSize = 300;
        private static readonly double _cubeZSize = 300;

        // variables to keep track of the FPS
        private int _fpsCount = 0;
        private int _frameTotalCount = 0;
        private DateTime _prevTime;

        // focal length and zoom, if these values are wrong, then the panorama will visually 'bend' at the corners of the cube-walls.
        private double _focalLength = 1500;
        private double _zoom = 500;
        private double _zoomPercentage;
        private double _zoomPercentageLast;
        private const int FOCAL_MIN =1200;
        private const int FOCAL_MAX = 5000;

        // camera rotation
        private double _xRotation = 0;//1002.2772;
        private double _yRotation = 0;
        private double _xRotationLast = 0;
        private double _yRotationLast = 0;
        private double _zRotation = 0; // Not used, this would roll the camera

        // variables for the mouse-drag movement.
        private bool _isMouseDown = false;
        private double _mouseXBegin = 0;
        private double _mouseYBegin = 0;
        private double _mouseX = 0;
        private double _mouseY = 0;
        private bool _needsUpdating = true; // True, to ensure the first update
        private bool _useTextureMap = true;

        // texture dimension
        private int _imageSize = 0;
        // fill the _vertices list with the cornerpoints of the cube.
        // the top and bottom plane will need to be moved 'inward' by 1 pixel to avoid antialias lines between planes. 
        // (this antialias problem can't be solved in a triangle)
        private List<Vertex3D> _vertices = new List<Vertex3D> {
                                         new Vertex3D(-_cubeXSize, -_cubeYSize, -_cubeZSize),
                                         new Vertex3D(_cubeXSize, -_cubeYSize, -_cubeZSize),
                                         new Vertex3D(-_cubeXSize, _cubeYSize, -_cubeZSize),
                                         new Vertex3D(_cubeXSize, _cubeYSize, -_cubeZSize),
                                         new Vertex3D(-_cubeXSize, -_cubeYSize, _cubeZSize),
                                         new Vertex3D(_cubeXSize, -_cubeYSize, _cubeZSize),
                                         new Vertex3D(-_cubeXSize, _cubeYSize, _cubeZSize),
                                         new Vertex3D(_cubeXSize, _cubeYSize, _cubeZSize),

                                         new Vertex3D(-_cubeXSize, -_cubeYSize+1.5, -_cubeZSize),
                                         new Vertex3D(_cubeXSize, -_cubeYSize+1.5, -_cubeZSize),
                                         new Vertex3D(-_cubeXSize, _cubeYSize-1.5, -_cubeZSize),
                                         new Vertex3D(_cubeXSize, _cubeYSize-1.5, -_cubeZSize),
                                         new Vertex3D(-_cubeXSize, -_cubeYSize+1.5, _cubeZSize),
                                         new Vertex3D(_cubeXSize, -_cubeYSize+1.5, _cubeZSize),
                                         new Vertex3D(-_cubeXSize, _cubeYSize-1.5, _cubeZSize),
                                         new Vertex3D(_cubeXSize, _cubeYSize-1.5, _cubeZSize)
                                     };

        // list of cube faces
        private List<Face> _faces;

        private Boolean _imagesLoaded = false;

        private Storyboard _panoramaCubeStoryBoard;
        private Panel _panoramaCubeCanvas;
        private TextBlock _debugText = new TextBlock();
        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="PanoramaCube"/> class.
        /// </summary>
        /// <param name="HostControl">The host control. This is necessary in beta 2 tot attach the keyboard eventhandler which can only be set on the </param>
        /// <param name="HostPanel">The host panel which hosts the control</param>
        /// <param name="ImageList">The list of image to render inside the cube.</param>
        /// <param name="ImageSize">The size of the images to render inside the cube. (this is the smallest value of width and height of the  images, all images will be displayed as a square)</param>
        public PanoramaCube(UserControl HostControl, Panel HostPanel, PanoramaImageList ImageList, int ImageSize)
        {
            this._imageSize = ImageSize;
            InitializeCube(HostControl, HostPanel, ImageList);
        }


        /// <summary>
        /// Initializes a new instance of the <see cref="PanoramaCube"/> class.
        /// </summary>
        /// <param name="HostControl">The host control. This is necessary in beta 2 tot attach the keyboard eventhandler which can only be set on the </param>
        /// <param name="HostPanel">The host panel which hosts the control</param>
        /// <param name="ImageList">The list of image to render inside the cube.</param>
        /// <param name="ImageSize">The size of the images to render inside the cube. (this is the smallest value of width and height of the  images, all images will be displayed as a square)</param>
        /// <param name="FPS">Desired framerate (will be calculated as 1000/FPS )</param>
        /// <param name="Subdivisions">Number of subdivisions of each panoramacube-face. A high subdivision level can cause Silverlight to stop responding</param>
        public PanoramaCube(UserControl HostControl, Panel HostPanel, PanoramaImageList ImageList, int FPS, int Subdivisions, int ImageSize)
        {
            this._imageSize = ImageSize;
            this.Subdivisions = Subdivisions;
            this.FPS = FPS;

            InitializeCube(HostControl, HostPanel, ImageList);
        }

        private void InitializeCube(UserControl HostControl, Panel HostPanel, PanoramaImageList ImageList)
        {
            HostPanel.Children.Add(_debugText);
            CreateStoryBoardAndEvents(HostPanel, HostControl);
            SetRotation(0, 0);
            SetZoom(50);


            if (ImageList.HasAllImages)
            {
                HostControl.KeyDown += new KeyEventHandler(Page_KeyDown);
                CreateFaces(_panoramaCubeCanvas, ImageList);
            }
            else
            {
                WriteText("ImageList should contain all image Uri's" + Environment.NewLine);
            }
        }


        private void CreateStoryBoardAndEvents(Panel HostPanel, UserControl HostControl)
        {
            this._panoramaCubeStoryBoard = new Storyboard();
            this._panoramaCubeStoryBoard.Completed += new EventHandler(PanoramaCubeStoryBoard_Completed);
            this._panoramaCubeStoryBoard.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 1000 / this.FPS));
            this._panoramaCubeStoryBoard.SetValue(FrameworkElement.NameProperty, "PanoramaCubeStoryBoard");
            this._panoramaCubeCanvas = HostPanel;

            this._panoramaCubeCanvas.Resources.Add("PanoramaCubeStoryBoard", _panoramaCubeStoryBoard);

            // setup mouse and keyboard events.
            HostControl.MouseMove += new MouseEventHandler(Page_MouseMove);
            HostControl.MouseLeftButtonDown += new MouseButtonEventHandler(Page_MouseDown);
            HostControl.MouseLeftButtonUp += new MouseButtonEventHandler(Page_MouseUp);
            HostPanel.KeyDown += new KeyEventHandler(Page_KeyDown);

            this.Loaded += new RoutedEventHandler(PanoramaCube_Loaded);
            
        }

        /// <summary>
        /// Create the faces of the panorama cube, and create a distortion object for every face
        /// </summary>
        /// <param name="HostPanel"></param>
        /// <param name="ImageList"></param>
        private void CreateFaces(Panel HostPanel, PanoramaImageList ImageList)
        {
            
            if (this.DebugEnabled)
            {
                _debugText.Visibility = Visibility.Visible;
            }
            else
            {
                _debugText.Visibility = Visibility.Collapsed;
            }

            Face frontFace = new Face("front", 2, 3, 1, 0, ImageList.FrontFaceImage);
            Face rightFace = new Face("right", 3, 7, 5, 1, ImageList.RightFaceImage);
            Face backFace = new Face("back", 7, 6, 4, 5, ImageList.BackFaceImage);
            Face leftFace = new Face("left", 6, 2, 0, 4, ImageList.LeftFaceImage);
            Face topFace = new Face("top", 8 + 6, 8 + 7, 8 + 3, 8 + 2, ImageList.TopFaceImage);
            Face bottomFace = new Face("bottom", 8 + 0, 8 + 1, 8 + 5, 8 + 4, ImageList.BottomFaceImage);


            if (
                frontFace != null
                && rightFace != null
                && backFace != null
                && leftFace != null
                && topFace != null
                && bottomFace != null)
            {


                // create a list of faces, 1 for every side of the cube, along with the image that needs to be mapped to this side
                _faces = new List<Face> {
                                        frontFace,
                                        rightFace,
                                        backFace,
                                        leftFace,
                                        topFace,
                                        bottomFace
                                    };

                foreach (Face thisFace in _faces)
                {
                    thisFace.distort = new DistortImage(_imageSize, _imageSize, this.Subdivisions, 1.0085, this._panoramaCubeCanvas, this.Width, this.Height, thisFace.TextureUri, true, thisFace.TextureBitmap);
                }
                DebugLogger.debugWrite("distort objects created");
                
            }
            else
            {
                WriteText("Incorrect image names used. Image names should end with:" + Environment.NewLine
                    + "_f" + Environment.NewLine
                    + "_r" + Environment.NewLine
                    + "_b" + Environment.NewLine
                    + "_l" + Environment.NewLine
                    + "_u" + Environment.NewLine
                    + "_d" + Environment.NewLine);
            }
        }

        private void WriteText(object text)
        {
            _debugText.Visibility = Visibility.Visible;
            _debugText.SetValue(Canvas.ZIndexProperty, 100);
            _debugText.SetValue(Canvas.LeftProperty, 10.0);
            _debugText.SetValue(Canvas.TopProperty, 10.0);
            _debugText.Foreground = new SolidColorBrush(Colors.Red);
            _debugText.Text += (string)text;            
        }

        /// <summary>
        /// Starts the 'gameloop' storyboard after all images are loaded, and attaches the Content.Resized event.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void PanoramaCube_Loaded(object sender, RoutedEventArgs e)
        {
            if (
                    (double.IsNaN(this.Width) || double.IsNaN(this.Height))
                )
            {
                this.Width = Application.Current.Host.Content.ActualWidth;
                this.Height = Application.Current.Host.Content.ActualHeight;
            }

            // debug logger 
            DebugLogger.addOutputTextBlock(_debugText);
            DebugLogger.enabled = true;
            DebugLogger.enableOutputLog(true);           

            this._prevTime = DateTime.Now;

            App.Current.Host.Content.Resized += new EventHandler(Content_Resized);
            // start the storyboard, this will trigger the screen updates
            this._panoramaCubeStoryBoard.Begin();
        }

        /// <summary>
        /// stores the current screen size when the component has been resized, also signals the panoramacube to render.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void Content_Resized(object sender, EventArgs e)
        {
            if (
                    (double.IsNaN(this.Width) || double.IsNaN(this.Height))
                )
            {
                Width = App.Current.Host.Content.ActualWidth;
                Height = App.Current.Host.Content.ActualHeight;
            }

            foreach (Face thisFace in _faces)
            {
                thisFace.distort.UpdateViewportSize(Width, Height);
            }

            _needsUpdating = true;
        }

        /// <summary>
        /// Handles the keyboard input.
        /// Responds to :
        ///    T    -   if enabled, switch between textured mode and polygon mode
        ///    +    -   increase zoom factor
        ///    -    -   decrease zoom factor
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Page_KeyDown(object sender, KeyEventArgs e)
        {
            // zoom the camera in and out when key + or - is pressed
            if (_enableRendermodeSwitch)
            {
                if (e.Key == Key.T)
                {
                    _useTextureMap = !_useTextureMap;
                    _needsUpdating = true;
                }
            }
            if (e.Key == Key.Add)
            {

                _zoomPercentage += 1;
                _needsUpdating = true;

            }
            if (e.Key == Key.Subtract)
            {
                _zoomPercentage -= 1;
                _needsUpdating = true;
            }
            CheckBounds();


        }

        /// <summary>
        /// Sets the _isMouseDown flag when the user presses the mouse key, the current position of the mouse
        /// is also stored which can be used to calculate relative movement.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void Page_MouseDown(object sender, MouseButtonEventArgs e)
        {
            // DebugLogger.debugWrite("CLICK");
            _isMouseDown = true;
            // store current mouse position to track relative movement
            _mouseXBegin = _mouseX;
            _mouseYBegin = _mouseY;
        }
        /// <summary>
        /// Clears the _isMouseDown flag when the user releases the mouse key
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void Page_MouseUp(object sender, MouseButtonEventArgs e)
        {
            // DebugLogger.debugWrite("CLICK");
            _isMouseDown = false;
        }

        /// <summary>
        /// store the mouse position when the mouse is moved
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void Page_MouseMove(object sender, MouseEventArgs e)
        {
            // store the mouse position
            _mouseX = e.GetPosition(this._panoramaCubeCanvas).X;
            _mouseY = e.GetPosition(this._panoramaCubeCanvas).Y;
        }

        /// <summary>
        /// Check if all images have been loaded, returns False if this is not the case.
        /// </summary>
        /// <returns></returns>
        private Boolean CheckImageLoadingStatus()
        {
            Boolean isCompleted = true;
            foreach (Face thisFace in this._faces)
            {

                // DebugLogger.debugWrite("distort:" + (thisFace.distort == null).ToString());
                if (!thisFace.distort.IsTextureLoaded)
                {
                    // if any of the textures is not loaded yet, set isCompleted to False
                    isCompleted = false;
                    continue;
                }
            }
            return isCompleted;
        }

        /// <summary>
        /// "Gameloop" routine that rotates the camera when needed, and renders starts panorama cube rendering.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void PanoramaCubeStoryBoard_Completed(object sender, EventArgs e)
        {
            if (_isMouseDown)
            {

                // rotate the X and Y axis according to the difference between the mouseposition 
                // at mouseDown, and the current mouse position
//                DebugLogger.debugWrite(_mouseX + "," + _mouseY + "\t" + _mouseXBegin + "  "+  _mouseYBegin );
                this._yRotation += (_mouseX - _mouseXBegin) / 100;
                this._xRotation += (_mouseY - _mouseYBegin) / 100;

                // limit the X-as rotation 
                CheckBounds();
                // DebugLogger.debugWrite("ROTATE : " + this._xRotation.ToString() + "," + this._yRotation.ToString());
                if (this._xRotation != this._xRotationLast || this._yRotation != this._yRotationLast)
                {
                    _needsUpdating = true;
                }
            }
            RotatePanoramaCubeCanvas_Render(sender, e);
        }

        /// <summary>
        /// Keep the X and Y rotation and the Zoom factor within limits;
        /// </summary>
        private void CheckBounds()
        {
            if (this._xRotation < -270) this._xRotation =-270;
            if (this._xRotation > -90) this._xRotation = -90;
            if (this._zoomPercentage < 1) this._zoomPercentage = 1;
            if (this._zoomPercentage > 100) this._zoomPercentage = 100;

            // make sure we stay in a specific range, otherwise the cube will be drawn incorrectly.


        }
        /// <summary>
        /// Render the panorama cube
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void RotatePanoramaCubeCanvas_Render(object sender, EventArgs e)
        {
            if (!_imagesLoaded)
            {
                _imagesLoaded = CheckImageLoadingStatus();
                DebugLogger.debugWrite(_imagesLoaded.ToString());
                if (_imagesLoaded)
                {
                    DebugLogger.debugWrite("Everything is loaded, ");
                }
            }
            else
            {
                /*
                 * if (!drawn)
                                {
                                    DebugLogger.debugWrite("teken");
                           //         drawn = true;
                                    _faces[0].distort.setTransform(
                                         0, 0, 300, 0, 400, 300, 0, 700
                                    );
                                }

                            }
                            if (false)
                            {
                */
              //  _needsUpdating = true;
                if (_needsUpdating)
                {
                    _zoomPercentageLast = _zoomPercentage;
                    _xRotationLast = _xRotation;
                    _yRotationLast = _yRotation;

                    // set the centre of the screen
                    double xoffset = this.Width / 2; ;
                    double yoffset = this.Height / 2; ;
                    _focalLength = FOCAL_MIN + ((FOCAL_MAX - FOCAL_MIN) / 100) * _zoomPercentage;
                    //DebugLogger.debugWrite("focal : " + _zoomPercentage + " " + GetRotation().ToString());


                    _needsUpdating = false;
                    this._panoramaCubeCanvas.Children.Clear();

                    if (this.Parent is Canvas)
                    {
                        xoffset = (int)(((Canvas)this.Parent).Width / 2);
                        yoffset = (int)(((Canvas)this.Parent).Height / 2);
                    }


                    // Calculate rotations for all the vertices
                    // precalc the sin/cos tables

                    //DebugLogger.debugWrite(this._xRotation.ToString() + "," + this._yRotation.ToString());
                    double mathSinX = Math.Sin(Math.PI / 180 * this._xRotation);
                    double mathCosX = Math.Cos(Math.PI / 180 * this._xRotation);

                    double mathSinY = Math.Sin(Math.PI / 180 * this._yRotation);
                    double mathCosY = Math.Cos(Math.PI / 180 * this._yRotation);

                    double mathSinZ = Math.Sin(Math.PI / 180 * this._zRotation);
                    double mathCosZ = Math.Cos(Math.PI / 180 * this._zRotation);

                    foreach (Vertex3D vertex in this._vertices)
                    {
                        // Rotate the vertex around the Z axis
                        double tempX1;
                        double tempY1;
                        double tempZ1;

                        tempY1 = (vertex.X * mathSinZ) + (vertex.Y * mathCosZ);
                        tempX1 = (vertex.X * mathCosZ) - (vertex.Y * mathSinZ);

                        // Rotate the vertex around the Y axis
                        vertex.RotatedX = (vertex.Z * mathSinY) + (tempX1 * mathCosY);
                        tempZ1 = (vertex.Z * mathCosY) - (tempX1 * mathSinY);

                        // Rotate the vertex around the X axis
                        vertex.RotatedZ = (tempY1 * mathSinX) + (tempZ1 * mathCosX);
                        vertex.RotatedY = (tempY1 * mathCosX) - (tempZ1 * mathSinX);

                        // Translate the vertex into a 2D coordinate
                        vertex.TranslatedX = (((vertex.RotatedX * _focalLength) / (vertex.RotatedZ + _zoom))) + xoffset;
                        vertex.TranslatedY = (((vertex.RotatedY * _focalLength) / (vertex.RotatedZ + _zoom))) + yoffset;
                    }

                    // Create polygons for Silverlight to work with from the newly calculated vertices
                    _debugText.Text = "";

                    foreach (Face thisFace in this._faces)
                    {

                        Vertex3D vertexA = this._vertices[thisFace.VertexA];
                        Vertex3D vertexB = this._vertices[thisFace.VertexB];
                        Vertex3D vertexC = this._vertices[thisFace.VertexC];
                        Vertex3D vertexD = this._vertices[thisFace.VertexD];


                        // Do a mixedproduct of all vertices for hidden surface removal
                        // Since our panorama-planes are always flat, we use a dotproduct of 3 of the square's vertices
                        // in this case, we want the INSIDE of the cube to be visible                   
                        double mixedProduct = (vertexB.TranslatedX - vertexA.TranslatedX) * (vertexC.TranslatedY - vertexA.TranslatedY) -
                                                (vertexC.TranslatedX - vertexA.TranslatedX) * (vertexB.TranslatedY - vertexA.TranslatedY);
                        bool isVisible = mixedProduct > 0;

                        if (isVisible)
                        {
                            // we only want to draw the faces that have 1 or more vertices in front of the camera
                            // i.e. where Z coord > 0
                            int visibleCount = 0;
                            if (vertexA.RotatedZ > 0) visibleCount++;
                            if (vertexB.RotatedZ > 0) visibleCount++;
                            if (vertexC.RotatedZ > 0) visibleCount++;
                            if (vertexD.RotatedZ > 0) visibleCount++;

                            // only draw the face if there's more than 0 vertex in front of the camera
                            if (visibleCount > 1)
                            {
                                thisFace.distort.setTransform(
                                                                 vertexA.TranslatedX, vertexA.TranslatedY,
                                                                 vertexB.TranslatedX, vertexB.TranslatedY,
                                                                 vertexC.TranslatedX, vertexC.TranslatedY,
                                                                 vertexD.TranslatedX, vertexD.TranslatedY,
                                                                 _useTextureMap
                                                             );
                            }
                        }
                    }
                }
                // Calculate the FPS (Frames Per Second) and display it
                DateTime now = DateTime.Now;
                TimeSpan frameTime = now.Subtract(this._prevTime);
                _fpsCount++;
                _frameTotalCount++;
                if (frameTime.Seconds >= 1)
                {
                    this._prevTime = now;
                    _fps = _fpsCount;
                    _fpsCount = 0;
                }
                this._debugText.Foreground = new SolidColorBrush(Colors.Black);
            }

            // restart the storyboard
            this._panoramaCubeStoryBoard.Begin();
        }
        /// <summary>
        /// Set camera rotation over X and Y Axes
        /// </summary>
        /// <param name="RotationX"></param>
        /// <param name="RotationY"></param>
        public void SetRotation(int RotationX, int RotationY)
        {
            this._xRotation = RotationX-180;
            this._yRotation = RotationY;
            CheckBounds();
            if (this._xRotation != this._xRotationLast || this._yRotation != this._yRotationLast)
            {
                _needsUpdating = true;
            }
        }

        /// <summary>
        /// Set the Zoom factor
        /// </summary>
        /// <param name="Zoom"></param>
        public void SetZoom(int Zoom)
        {
            this._zoomPercentage = Zoom;
            if (this._zoomPercentage != this._zoomPercentageLast)
            {
                _needsUpdating = true;
            }
            CheckBounds();
        }
        /// <summary>
        /// Get the current camera rotation 
        /// </summary>
        /// <returns></returns>
        public Point GetRotation()
        {
            return new Point(this._xRotation+180, this._yRotation);
        }
        /// <summary>
        /// Get the current Zoom factor
        /// </summary>
        /// <returns></returns>
        public int GetZoom()
        {
            return (Convert.ToInt16(_zoomPercentage));
        }

       

        
    }
}
